Программирование и научные вычисления на языке Python/§15
Классы собирают в себе наборы данных (переменных) вместе с наборами функций на них действующими. Цель состоит в том, чтобы достигнуть более модульного кода с помощью группировки переменных и функций в легко изменяемые (чаще всего небольшие) узлы. Многие проблемы могут быть легко решены и без помощи классов, особенно когда мы рассматриваем такие небольшие примеры, как в этом курсе. Но в многих задачах классы оказываются наиболее элегантным решением, с которым существенно легче работать на поздних стадиях. Кроме того, там где проблемы не носят такой алгоритмической основы, как в научных проблемах, классы даже помогают лучше понять проблему, определив структуру задачи. И как следствие, большая часть крупного программного обеспечения написана с применением классов. Программирование с классами поддерживается большинством современных языков, в том числе и Python. Простые классы функций Классы могут быть использованы в научных вычислениях для решения многих задач, но чаще всего они востребованы в представлении математических функций с некоторым набором параметров и одной или несколькими независимыми переменными. Функцией с параметрами является, например, наша самая первая функция y(t) = v_0(t) - \frac{1}{2}gt^2 . Здесь y'' является функций времени ''t и кроме того, зависит от других параметров v0 и g''. Мы могли бы придумать какое-то новое обозначение, вроде ''y(t; ''v0, g''), чтобы показать, что ''t является независимой переменной, а v0 и g'' задаваемыми параметрами. При этом, строго говоря для Земли ''g, гравитационная постоянная, неизменна, то есть правильнее было бы писать 'y''(t; v0). В общем случае, у нас может иметься функция, которая будет записываться f''(''x; p1, ..., pn). Как нам лучше интерпретировать такие математические функции в виде программных? Первое очевидное решение получать и переменные, и изменяемые параметры как аргументы обычной функции: def y(t, v0): g = 9.81 return v0*t - 0.5*g*t**2 Проблема в этом случае состоит в том, что множество инструментов, что мы используем для математических операций с функциями предполагают, что функция одной переменной должна принимать в своем компьютерном представлении только один аргумент. Например, у нас есть инструмент для дифференцирования f(x) в точке x'', которое осуществляется с помощью приближения f' \approx \frac{f(x+h)-f(x)}{h} и записывается в виде кода def diff(f, x, h=1E-10): return (f(x+h) - f(x))/h И наша ясная функция diff легко работает с функциями, принимающими один аргумент: def h(t): return t**4 + 4*t dh = diff(h, 0.1) from math import sin, pi x = 2*pi dsin = diff(sin, x, h=1E-9) Но, к несчастью, diff не будет работать с нашей функцией y(t, v0). Вызов diff(y, t) приведет к ошибке в функции diff, поскольку дифференцируемая функция должна принимать лишь один агумент, а принимает два. Написание альтернативной diff-функции для f с двумя аргументами это плохое решение, поскольку оно ограничивает всевозможные f до функций с одной переменной и одним аргументом. Фундаментальные принципы программирования гласят, что следует стремиться к такому решению, которое будет настолько общим и настолько широко применимым, насколько это возможно. В настоящем случае это означает, что функция diff должна быть применима к любой функции одной переменной. Плохое решение: Глобальные переменные Требования к представлению функций таким образом состоит в том, чтобы они принимали только независимую переменную, то есть, получается, выглядели так: def y(t): g = 9.81 return v0*t - 0.5*g*t**2 Но поскольку v0 не определено, то вызов функции требует того, чтобы переменная была заранее определена и тогда мы уже можем определить значение для производной: v0 = 3 dy = diff(y, 1) Но использование глобальных переменных в этом случае это плохой стиль программирования. Почему это плохо, можно проиллюстрировать на примере когда нам нужно использовать разные версии одной функции. Например, мы бросаем мячик вверх со скоростями 1 и 5 м/с. Каждый раз, когда мы вызываем ''y, нам понадобиться задавать перед ним новое значение v0: v0 = 1; r1 = y(t) v0 = 5; r2 = y(t) Другая проблема в том, что переменные с такими простыми именами как v0 могут легко быть использованы в других частях программы. По этим и другим причинам может уже сейчас выступить золотое правило программирования: сокращать число глобальных переменных настолько, насколько это возможно. Итак, есть ли лекарство от нашей болезни? Ответ: да, и рецепт его расписан ниже. Представление функции в виде класса Класс заключает в себе набор переменных (данных) и набор функций, связанных в единое целое. Переменные видны изнутри класса всем его функциям. То есть они «глобальные» для функций своего класса. Класс похож на модуль, но находящийся в тексте самой программы. Но при этом по технике его использования он существенно отличается. Например, вы можете создать множество копий одного класса, в то время как модуль выступает в единственном числе. Когда мы получше познакомимся с классами, вы и сами увидите схожие моменты и отличия. А сейчас продолжим рассмотрение нашего примера. Обращаясь к нашей функции 'y''(t; v0) мы можем сказать, что переменные v0 и g определяют данные, а t служит аргументом некоторой функции Python value(t). Программист, практикующий классы, соберет данные v0 и g и функцию value(t) вместе в один класс. К тому же класс обычно содержит и другую функцию называемую конструктором (constructor) для инициализации данных. Конструктор всегда носит имя __init__. Каждый класс имеет имя, которое традиционно начинают с большой буквы, поэтому для нашего класса мы выберем имя Y, соотнося его таким образом с y'' для математической функции. Реализация Законченный код для нашего класса Y выглядит следующим образом: class Y: def __init__(self, v0): self.v0 = v0 self.g = 9.81 def value(self, t): return self.v0*t - 0.5*self.g*t**2 Головоломкой для новичков в классах обычно оказывается параметр self, который поэтому для своего понимания может потребовать немного усилий и времени. Использование Перед тем как мы станем разбираться с тем как этот класс сделан, начнем с того, что покажем как он может использоваться. Класс создает новый тип данных, так что у нас теперь есть тип данных Y, с помощью которого мы можем создавать объекты. Объекты определенного пользователем класса (как Y) мы будем называть экземплярами. Следующее выражение создает экземпляр класса Y: y = Y(3) Казалось бы, мы вызвали класс Y как будто это обычная функция. Однако, Y(3) автоматически представляется Python как вызов конструктора __init__ в классе Y. Аргументы при вызове, здесь это только число 3, всегда принимаются как аргументы функции-конструктора __init__ следующие после всегда стоящего на первом месте аргумента self. Имея на руках экземпляр y, мы можем узнать значение ''y(t''=0.1; ''v0=3) с помощью инструкции v = y.value(0.1) Теперь, поскольку происходит вызов value, аргумент self оказывается в стороне. Чтобы обратиться к функциям или переменным класса, нужно указывать префикс этой функции или имени переменной. Например, так мы можем вывести значение v0 экземпляра y: print y.v0 В этом случае на выходе мы увидим число 3. Кроме термина «экземпляр» для объектов, рожденных классом, говорят о функциях класса как методах и переменных класса как атрибутах. С этого момента мы будем пользоваться такой терминологией. В нашем простом классе Y имеются два метода: __init__ и value и два атрибута: v0 и g. Имена методов и атрибутов могут свободно меняться точно так же как имена обычных функций и переменных. Однако, конструктор обязательно должен называться __init__. Переменная self Внутри конструктора __init__ аргумент self это переменная, содержащая создаваемый экземпляр. Когда мы пишем self.v0 = v0 self.g = 9.81 мы определяем два новых атрибута в этом экземпляре. Записывая y = Y(3) мы не только передаем число, но и имя экземпляра, то есть этот вызов можно представить как Y.__init__(y, 3) Когда мы пишем в теле конструктора self.v0 = v0, мы в действительности инициализируем y.v0. Когда же пишем value = y.value(0.1) Python переводит это как вызов value = y.value(y, 0.1) Выражение внутри метода value self.v0*t - 0.5*self.g*t**2 ввиду того, что self это y имеет смысл тот же, что y.v0*t - 0.5*y.g*t**2 Правила касательно self следующие: * Любой метод класса содержит self в качестве первого аргумента. * self представляет в своем лице (произвольный) экземпляр класса. * Другой метод или атрибут класса используют self в виде self.name, где name имя этого атрибута или метода. * self в качестве аргумента пропускается при вызове методов класса Расширение класса В классе мы можем иметь так много атрибутов и методов, как захотим, так что давайте добавим новый метод к классу Y. Этот метод назовем formula он будет выводить строку, содержащую формулу математической функции y''. После этой формулы мы выводим значение ''v0: 'v0*t - 0.5*g*t**2; v0=%g' % self.v0 где self это экземпляр класса Y. Вызов formula не требует никаких аргументов: print y.formula() Однако, из правил о self мы помним, что хотя метод formula при вызове и не требует никаких аргументов, но при определении мы должны передать ему аргумент self: def formula(self): 'v0*t - 0.5*g*t**2; v0=%g' % self.v0 Теперь наш класс целиком выглядит так: class Y: """The vertical motion of a ball.""" def __init__(self, v0): self.v0 = v0 self.g = 9.81 def value(self, t): return self.v0*t - 0.5*self.g*t**2 def formula(self): 'v0*t - 0.5*g*t**2; v0=%g' % self.v0 И пример того как может использоваться: y = Y(5) t = 0.2 v = y.value(t) print 'y(t=%g; v0=%g) = %g' % (t, y.v0, v) print y.formula() Результат: y(t=0.2; v0=5) = 0.8038 v0*t - 0.5*g*t**2; v0=5 Методы как обычные функции Использование класса позволяет создать несколько функций y с разными значениями v0: y1 = Y(1) y2 = Y(1.5) y3 = Y(-3) При этом мы можем использовать y1.value, y2.value и y3.value как обычные функции от t, а значит и применять все то же, что имеется их для любых других функций одной переменной. Например, наше объяснение введения классов мы начали с примера взятия производной в точке: dy1dt = diff(y1.value, 0.1) dy2dt = diff(y2.value, 0.1) dy3dt = diff(y3.value, 0.2) Строки документации Классы, как и функции, могут быть описаны простым человеческим языком сразу в следующей строке после заголовка с помощью doc strings — строк документации. Вводятся они абсолютно таким же образом, с помощью тройки двойных кавычек с каждой стороны: class Y: """The vertical motion of a ball.""" def __init__(self, v0): ... В случае объемного конечного продукта обычно пишут более исчерпывающее объяснение о том как этот класс может быть использован, какие методы и атрибуты включает, примеры использования класса: class Y: """Mathematical function for the vertical motion of a ball. Methods: constructor(v0): set initial velocity v0. value(t): compute the height as function of t. formula(): print out the formula for the height. Attributes: v0: the initial velocity of the ball (time 0). g: acceleration of gravity (fixed). Usage: >>> y = Y(3) >>> position1 = y.value(0.1) >>> position2 = y.value(0.3) >>> print y.formula() v0*t - 0.5*g*t**2; v0=3 """ Альтернативная реализация классов функций Чтобы далее продолжить знакомство с программированием с участием классов, теперь мы реализуем класс Y другим образом. Это хорошая привычка всегда в классе иметь конструктор и инициализировать в нем атрибуты класса. Но это не обязательное требование. Давайте выбросим конструктор и представим v0 как аргумент метода value. Если пользователь при вызове не задает v0, то мы используем значение из более ранних вызовов, находящееся в атрибуте self.v0. О том, задал ли пользователь v0 или нет, мы узнаем, задав в определении v0 значение по умолчанию None, а дальше проверяя его с помощью if. Наша альтернативная реализация представлена теперь классом Y2: class Y2: def value(self, t, v0=None): if v0 is not None: self.v0 = v0 g = 9.81 return self.v0*t - 0.5*g*t**2 В этот раз у класса только один метод и один атрибут, поскольку мы обошлись без конструктора, а g сделали локальной переменной метода value. Но если здесь нет конструктора, то как же создается экземпляр? На самом деле Python создает пустой конструктор. Это позволяет нам написать как и раньше: y = Y2() чтобы создать экземпляр y. Поскольку в автоматически сгенерированном пустом конструкторе ничего не происходит, то на этом этапе y не получает никаких атрибутов. Написав print y.v0 мы получим ошибку: AttributeError: Y2 instance has no attribute 'v0' Но при вызове v = y.value(0.1, 5) мы создаем атрибут self.v0 в методе value. Теперь print y.v0 дает 5. Это значение v0 используется пока новый вызов не изменит его. Возникающее исключение AttributeError следовало бы учесть в теле класса (а еще точнее методе value) с помощью блока try-except: class Y2: def value(self, t, v0=None): if v0 is not None: self.v0 = v0 g = 9.81 try: value = self.v0*t - 0.5*g*t**2 except AttributeError: msg = 'You cannot call value(t) without first ' 'calling value(t, v0) to set v0' raise TypeError(msg) return value Конечно, класс Y это лучшая реализация, чем Y2, поскольку имеет более простую форму. Как уже отмечалось, использование конструктора это хорошая привычка программирования, конструктор осуществляет удобную связь между «внешним миром» и классом. Цель нашего класса Y2 только в том чтобы показать что Python обладает большой гибкостью к определению атрибутов и что вообще нет специальных требований что именно класс должен содержать. Классы без классов Новичкам в концепции классов часто бывает сложно понять, в чем она вообще состоит. Вообще этот урок мог оказаться для вас весьма утомительным. Может вообще оказаться, что к программированию с помощью классов вы придете и гораздо позже, чем окончите этот курс. И об этом не стоит переживать. Класс содержит набор переменных (данных) и набор методов (функций). Набор переменных уникален для каждого экземпляра класса. То есть, если вы создадите десять экземпляров, каждый из них имеет свои переменные. Эти переменные можно представить как словарь, в котором ключами служат названия переменных. Каждый экземпляр тогда имеет свой словарь и, грубо говоря, мы можем рассматривать экземпляр как такой словарь. С другой стороны, методы у всех экземпляров общие. Метод касса можно представить как обычную глобальную функцию, принимающую экземпляр в форме словаря как первый аргумент. Метод далее обращается к переменным в экземпляре (словаре), указанным при вызове. Для класса Y и экземпляра y, методы это обычные функции со следующими именами и аргументами: Y.value(y, t) Y.formula(y) Класс представляется как пространство имен, то есть все его функции должны иметь префикс Y. Два разных класса, скажем С1 и С2 могут иметь функции с одним и тем же именем, например value, но при этом поскольку они относятся к разным классам, их имена становятся различны: С1.value и С2.value. Модули также представляют собой пространства имен для своих функций и переменных (math.sin, cmath.sin, numpy.sin) Единственным отличием конструктора класса в Python является то, что он позволяет нам использовать другой синтаксис для вызова методов: y.value(t) y.formula() Мы можем легко реализовать концепцию класса и без него самого. Как мы уже выяснили, все что нам нужно это словарь и обычные функции. Наш класс Y может быть реализован и так: def value(self, t): return self'v0'*t - 0.5*self'g'*t**2 def formula(self): print 'v0*t - 0.5*g*t**2; v0=%g' % self'v0' Представим эти две функции расположены в модуле Y: import Y y = {'v0': 4, 'g': 9.81} # создаем "экземпляр" y1 = Y.value(y, t) Теперь у нас нет вообще никакого конструктора, поскольку нет и класса. Инициализация происходит при создании словаря y, но мы можем включить инициализацию и в модуль Y: def init(v0): return {'v0': v0, 'g': 9.81} Использование такого модуля-класса теперь выглядит более похожим на обычное: import Y y = Y.init(4) y1 = Y.value(y, t) И такая реализация вполне возможна и существует. На самом деле любой класс в Python имеет словарь-атрибут __dict__, который хранить все имеющиеся в экземпляре переменные: >>> y = Y(1.2) >>> print y.__dict__ {'v0': 1.2, 'g': 9.8100000000000005} Итак, в этом уроке мы рассмотрели классы с технической точки зрения. Следующий урок скорее посвящен классам как пути моделирования в терминах данных и операциях над данными. Ссылки Объектно-ориентированное программирование на Питоне Категория:Программирование и научные вычисления на языке Python